package com.itranswarp.learnjava.practice.plugin.views;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.part.*;

import com.itranswarp.javapractice.plugin.Activator;
import com.itranswarp.javapractice.plugin.data.GitBlob;
import com.itranswarp.javapractice.plugin.data.GitCommit;
import com.itranswarp.javapractice.plugin.data.GitNode;
import com.itranswarp.javapractice.plugin.data.GitTree;
import com.itranswarp.javapractice.plugin.data.LoadingNode;
import com.itranswarp.javapractice.plugin.data.Node;
import com.itranswarp.javapractice.plugin.repackaged.com.google.gson.GsonBuilder;
import com.itranswarp.learnjava.practice.plugin.views.Utils.HttpResponse;

import org.eclipse.jface.viewers.*;
import org.eclipse.jface.action.*;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.ui.*;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.SWT;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;

/**
 * This sample class demonstrates how to plug-in a new workbench view. The view
 * shows data obtained from the model. The sample creates a dummy model on the
 * fly, but a real implementation would connect to the model available either in
 * this or another plug-in (e.g. the workspace). The view is connected to the
 * model using a content provider.
 * <p>
 * The view uses a label provider to define how model objects should be
 * presented in the view. Each view can present the same model objects using
 * different labels and icons, if needed. Alternatively, a single label provider
 * can be shared between views in order to ensure that objects of the same type
 * are presented in the same way everywhere.
 * <p>
 */

public class PracticeView extends ViewPart {

	/**
	 * The ID of the view as specified by the extension.
	 */
	public static final String ID = "com.itranswarp.learnjava.practice.plugin.views.PracticeView";

	private TreeViewer viewer;
	private PracticeContentProvider contentProvider;
	private Action refreshAction;
	private Action browseAction;
	private Action importAction;
	private Action doubleClickAction;

	@Override
	public void createPartControl(Composite parent) {
		this.viewer = new TreeViewer(parent, SWT.H_SCROLL | SWT.V_SCROLL);
		// drillDownAdapter = new DrillDownAdapter(viewer);
		this.contentProvider = new PracticeContentProvider(getViewSite());
		this.viewer.setContentProvider(contentProvider);
		this.viewer.setInput(getViewSite());
		this.viewer.setLabelProvider(new PracticeLabelProvider(PlatformUI.getWorkbench().getSharedImages()));

		// Create the help context id for the viewer's control
		// this.workbench.getHelpSystem().setHelp(viewer.getControl(),
		// "com.itranswarp.practice.plugin.viewer");

		getSite().setSelectionProvider(this.viewer);
		makeActions();
		hookContextMenu();
		hookDoubleClickAction();
		contributeToActionBars();
		// init tree:
		Node initNode = loadTreeContent();
		if (initNode == null) {
			this.contentProvider.setRootChild(new LoadingNode());
			asyncRefresh();
		} else {
			this.contentProvider.setRootChild(initNode);
		}
		this.viewer.refresh();
		this.viewer.expandAll();
	}

	void asyncRefresh() {
		// start async job:
		System.out.println("start async refresh...");
		Job job = new Job("Download practice list.") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				monitor.beginTask("Download practice list.", 100);
				try {
					String lastSha = httpGetLastCommit();
					System.out.println("last sha: " + lastSha);
					HttpResponse resp = Utils.http("GET",
							"https://gitee.com/api/v5/repos/liaoxuefeng/learn-java/git/trees/master?recursive=1&t="
									+ System.currentTimeMillis(),
							monitor, null, null);
					String json = new String(resp.body, StandardCharsets.UTF_8);
					Node node = fromJson(json);
					Display.getDefault().asyncExec(() -> {
						System.out.println("update ok!");
						contentProvider.setRootChild(node);
						viewer.refresh();
						viewer.expandAll();
						Activator.getDefault().setJson(json);
						Activator.getDefault().setLastSha(lastSha);
					});
					return new Status(Status.OK, Activator.PLUGIN_ID, Status.OK, "Update practice list OK.", null);
				} catch (Exception e) {
					Display.getDefault().asyncExec(() -> {
						contentProvider.setRootChild(new LoadingNode(true));
						viewer.refresh();
					});
					return new Status(Status.WARNING, Activator.PLUGIN_ID, Status.ERROR, "Download failed.", e);
				}
			}
		};
		job.schedule();
	}

	String httpGetLastCommit() {
		try {
			HttpResponse response = Utils.http("GET",
					"https://gitee.com/api/v5/repos/liaoxuefeng/learn-java/commits?page=1&per_page=1&t="
							+ System.currentTimeMillis(),
					null, null, null);
			if (response.isOK()) {
				String json = new String(response.body, StandardCharsets.UTF_8);
				GitCommit[] commits = new GsonBuilder().create().fromJson(json, GitCommit[].class);
				if (commits.length > 0) {
					GitCommit commit = commits[0];
					return commit.getSha();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	void downloadAndImportPractice(GitNode node) {
		String downloadUrl = node.getUrl();
		System.out.println("download from: " + downloadUrl);
		String name = node.getDisplayName();
		// remove ".zip":
		String importProjectName = name.substring(0, name.length() - 4);
		// start async job:
		Job job = new Job("Download and import project: " + importProjectName) {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				monitor.beginTask("Download and import project: " + importProjectName, 100);
				File tmpFile;
				try {
					tmpFile = File.createTempFile("JPLXF", ".zip");
					System.out.println("download to file: " + tmpFile.getAbsolutePath());
					tmpFile.deleteOnExit();
				} catch (IOException e) {
					return new Status(Status.ERROR, "unknown", Status.ERROR, "Cannot create temp file.", e);
				}
				ByteArrayOutputStream buffer = new ByteArrayOutputStream(4096);
				try (BufferedOutputStream output = new BufferedOutputStream(buffer)) {
					HttpResponse resp = Utils.http("GET", downloadUrl, monitor, null, null);
					if (!resp.isOK()) {
						throw new IOException("Bad response: " + resp.status);
					}
					output.write(resp.body);
				} catch (IOException e) {
					if (monitor.isCanceled()) {
						return Status.CANCEL_STATUS;
					}
					return new Status(Status.ERROR, "unknown", Status.ERROR, "Download failed.", e);
				}
				if (monitor.isCanceled()) {
					return Status.CANCEL_STATUS;
				}
				String json = new String(buffer.toByteArray(), StandardCharsets.UTF_8);
				GitBlob blob = new GsonBuilder().create().fromJson(json, GitBlob.class);
				try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(tmpFile))) {
					Arrays.stream(blob.getContent().split("\n")).forEach(line -> {
						try {
							output.write(Base64.getDecoder().decode(line));
						} catch (IOException e) {
							throw new RuntimeException(e);
						}
					});
				} catch (Exception e) {
					if (monitor.isCanceled()) {
						return Status.CANCEL_STATUS;
					}
					return new Status(Status.ERROR, "unknown", Status.ERROR, "Download failed.", e);
				}
				Display.getDefault().asyncExec(new Runnable() {
					public void run() {
						importPractice(importProjectName, tmpFile);
					}
				});
				monitor.worked(10);
				return Status.OK_STATUS;
			}
		};
		job.schedule();
	}

	void importPractice(String importProjectName, File zipFile) {
		final IWorkspace workspace = ResourcesPlugin.getWorkspace();
		final IPath workspacePath = workspace.getRoot().getLocation();
		// check if project already exist:
		for (IProject p : workspace.getRoot().getProjects()) {
			if (importProjectName.equals(p.getName())) {
				showError("当前Workspace已经存在一个名称为“" + importProjectName + "”的Project，请删除或重命名后再尝试导入。");
				return;
			}
		}
		if (!showConfirm("是否下载并导入“" + importProjectName + "”？")) {
			return;
		}
		try {
			// unzip to path:
			if (!unzip(importProjectName, zipFile.getAbsolutePath(), workspacePath)) {
				showMessage("已取消导入Project“" + importProjectName + "”");
				return;
			}
			IPath path = workspacePath.append(new Path(importProjectName)).append(new Path(".project"));
			IProjectDescription description = workspace.loadProjectDescription(path);
			IProject project = workspace.getRoot().getProject(description.getName());
			project.create(description, null);
			project.open(null);
			IStatus status = new Status(IStatus.INFO, "Java Practice",
					"Project " + importProjectName + " was imported into workspace.");
			Activator.getDefault().getLog().log(status);
			showMessage("已成功导入Project：" + description.getName());
		} catch (Exception e) {
			e.printStackTrace();
			showError("导入Project失败：" + e.getMessage());
		}
	}

	// load tree content on startup:
	Node loadTreeContent() {
		String json = Activator.getDefault().getJson();
		if (json != null && !json.isEmpty()) {
			return fromJson(json);
		}
		return null;
	}

	Node fromJson(String json) {
		try {
			GitTree gitTree = new GsonBuilder().create().fromJson(json, GitTree.class);
			GitNode[] gitNodes = gitTree.getTree().stream().filter(n -> n.getPath().startsWith("practices"))
					.filter(n -> "tree".equals(n.getType()) || "blob".equals(n.getType())).sorted((n1, n2) -> {
						return n1.getPath().compareTo(n2.getPath());
					}).map(GitNode::init).toArray(GitNode[]::new);
			GitNode root = Arrays.stream(gitNodes).filter(n -> n.getPath().equals("practices")).findFirst().get();
			for (GitNode gitNode : gitNodes) {
				root.addChild(gitNode);
			}
			if (root.hasChildren()) {
				return root.getChildren().get(0);
			}
		} catch (Exception e) {
			throw new RuntimeException("Parse JSON failed.", e);
		}
		throw new RuntimeException("Parse JSON failed.");
	}

	boolean unzip(String importProjectName, String importProjectZip, IPath workspacePath) throws IOException {
		File zipPath = workspacePath.toFile();
		if (new File(zipPath, importProjectName).isDirectory()) {
			if (showConfirm("当前Workspace已存在目录“" + importProjectName + "”，是否删除后再导入？")) {
				deleteDir(new File(zipPath, importProjectName));
			} else {
				return false;
			}
		}
		try (ZipFile zipFile = new ZipFile(importProjectZip)) {
			Enumeration<? extends ZipEntry> entries = zipFile.entries();
			while (entries.hasMoreElements()) {
				ZipEntry entry = entries.nextElement();
				if (entry.isDirectory()) {
					File dir = new File(zipPath, entry.getName());
					System.err.println("Extracting directory: " + entry.getName() + " to: " + dir.getPath());
					dir.mkdirs();
				} else {
					File file = new File(zipPath, entry.getName());
					System.err.println("Extracting file: " + entry.getName() + " to: " + file.getPath());
					try (InputStream zipInput = zipFile.getInputStream(entry)) {
						byte[] buffer = new byte[10240];
						try (OutputStream output = new BufferedOutputStream(new FileOutputStream(file))) {
							for (;;) {
								int n = zipInput.read(buffer);
								if (n == (-1)) {
									break;
								}
								output.write(buffer, 0, n);
							}
						}
					}
				}
			}
		}
		return true;
	}

	void deleteDir(File dir) {
		if (dir.isDirectory()) {
			for (File file : dir.listFiles()) {
				deleteDir(file);
			}
		} else {
			System.err.println("Deleting " + dir.getPath());
			dir.delete();
		}
	}

	void openUrl(String url) {
		try {
			PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(url));
		} catch (PartInitException | MalformedURLException e) {
			e.printStackTrace();
		}
	}

	private void hookContextMenu() {
		MenuManager menuMgr = new MenuManager("#PopupMenu");
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager manager) {
				PracticeView.this.fillContextMenu(manager);
			}
		});
		Menu menu = menuMgr.createContextMenu(viewer.getControl());
		viewer.getControl().setMenu(menu);
		getSite().registerContextMenu(menuMgr, viewer);
	}

	private void contributeToActionBars() {
		IActionBars bars = getViewSite().getActionBars();
		fillLocalPullDown(bars.getMenuManager());
		fillLocalToolBar(bars.getToolBarManager());
	}

	private void fillLocalPullDown(IMenuManager manager) {
		manager.add(refreshAction);
		manager.add(new Separator());
		manager.add(browseAction);
		manager.add(importAction);
	}

	private void fillContextMenu(IMenuManager manager) {
		manager.add(refreshAction);
		manager.add(browseAction);
		manager.add(importAction);
		manager.add(new Separator());
		// Other plug-ins can contribute there actions here
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
	}

	private void fillLocalToolBar(IToolBarManager manager) {
		manager.add(refreshAction);
		manager.add(browseAction);
		manager.add(importAction);
	}

	private void makeActions() {
		refreshAction = new Action() {
			public void run() {
				contentProvider.setRootChild(new LoadingNode());
				viewer.refresh();
				asyncRefresh();
			}
		};
		refreshAction.setText("Refresh");
		refreshAction.setToolTipText("Refresh practice list");
		refreshAction.setImageDescriptor(
				PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(ISharedImages.IMG_ELCL_SYNCED));

		browseAction = new Action() {
			public void run() {
				IStructuredSelection selection = viewer.getStructuredSelection();
				Object o = selection.getFirstElement();
				if (o instanceof GitNode) {
					GitNode git = (GitNode) o;
					if (git.isPractice()) {
						Node parent = git.getParent();
						if (parent instanceof GitNode) {
							String url = ((GitNode) parent).getBrowerUrl();
							if (url != null) {
								openUrl(url);
							}
						}
					} else {
						String url = git.getBrowerUrl();
						if (url != null) {
							openUrl(url);
						}
					}
				}
			}
		};
		browseAction.setText("Browse");
		browseAction.setToolTipText("Browse tutorial online");
		browseAction.setImageDescriptor(
				PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(ISharedImages.IMG_ETOOL_HOME_NAV));

		importAction = new Action() {
			public void run() {
				IStructuredSelection selection = viewer.getStructuredSelection();
				Object o = selection.getFirstElement();
				if (o instanceof GitNode) {
					GitNode git = (GitNode) o;
					if (git.isPractice()) {
						downloadAndImportPractice(git);
					}
				}
			}
		};
		importAction.setText("Import");
		importAction.setToolTipText("Download and import practice");
		importAction.setImageDescriptor(
				PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(ISharedImages.IMG_OBJ_ADD));

		doubleClickAction = new Action() {
			public void run() {
				IStructuredSelection selection = viewer.getStructuredSelection();
				Object o = selection.getFirstElement();
				if (o instanceof LoadingNode) {
					LoadingNode loading = (LoadingNode) o;
					if (loading.isNetworkError()) {
						asyncRefresh();
					}
				}
				if (o instanceof GitNode) {
					GitNode git = (GitNode) o;
					if (git.isPractice()) {
						downloadAndImportPractice(git);
					} else {
						String url = git.getBrowerUrl();
						if (url != null) {
							openUrl(url);
						}
					}
				}
			}
		};
	}

	private void hookDoubleClickAction() {
		viewer.addDoubleClickListener(new IDoubleClickListener() {
			public void doubleClick(DoubleClickEvent event) {
				doubleClickAction.run();
			}
		});
	}

	private void showMessage(String message) {
		MessageDialog.openInformation(viewer.getControl().getShell(), "Java Practice", message);
	}

	private void showError(String message) {
		MessageDialog.openError(viewer.getControl().getShell(), "Java Practice", message);
	}

	private boolean showConfirm(String message) {
		return MessageDialog.openConfirm(viewer.getControl().getShell(), "Java Practice", message);
	}

	@Override
	public void setFocus() {
		viewer.getControl().setFocus();
	}
}
